package registry

import (
	"context"
	"encoding/json"
	"fmt"
	"os"
	"strings"
	"time"

	"github.com/containers/image/v5/docker"
	"github.com/containers/image/v5/image"
	_ "github.com/containers/image/v5/image"
	"github.com/containers/image/v5/transports/alltransports"
	"github.com/containers/image/v5/types"

	"gopkg.openfuyao.cn/bkeadm/utils"
	"gopkg.openfuyao.cn/bkeadm/utils/log"
)

const (
	// DockerV2Schema2MediaType MIME type represents Docker manifest schema 2
	DockerV2Schema2MediaType = "application/vnd.docker.distribution.manifest.v2+json"
	// DockerV2ListMediaType MIME type represents Docker manifest schema 2 list
	DockerV2ListMediaType = "application/vnd.docker.distribution.manifest.list.v2+json"

	// DockerV1Schema2MediaType MIME type represents Docker manifest schema 1
	DockerV1Schema2MediaType = "application/vnd.oci.image.manifest.v1+json"
	// DockerV1ListMediaType MIME type represents Docker manifest schema 1 list
	DockerV1ListMediaType = "application/vnd.oci.image.index.v1+json"
)

type ImageArch struct {
	Name         string `json:"name"`
	OS           string `json:"os"`
	Architecture string `json:"architecture"`
}

type DockerV2List struct {
	SchemaVersion int        `json:"schemaVersion"`
	MediaType     string     `json:"mediaType"`
	Manifests     []Manifest `json:"manifests"`
}
type Manifest struct {
	MediaType string `json:"mediaType"`
	Digest    string `json:"digest"`
	Size      int    `json:"size"`
	Platform  struct {
		Architecture string `json:"architecture"`
		OS           string `json:"OS"`
	} `json:"platform"`
}

type DockerV2Schema struct {
	SchemaVersion int    `json:"schemaVersion"`
	MediaType     string `json:"mediaType"`
	Config        struct {
		MediaType string `json:"mediaType"`
		Size      int    `json:"size"`
		Digest    string `json:"digest"`
	} `json:"config"`
	Layers []struct {
		MediaType string `json:"mediaType"`
		Size      int    `json:"size"`
		Digest    string `json:"digest"`
	}
}

func (op *Options) Inspect() {
	ctx := context.Background()

	destinationCtx, err := newSystemContext()
	if err != nil {
		log.Error(err)
		return
	}
	if op.DestTLSVerify {
		destinationCtx.DockerInsecureSkipTLSVerify = types.NewOptionalBool(true)
		destinationCtx.DockerDaemonInsecureSkipTLSVerify = true
	}
	imageName := op.Image
	if !strings.HasPrefix(op.Image, "docker://") {
		imageName = "docker://" + op.Image
	}

	ref, err := alltransports.ParseImageName(imageName)
	if err != nil {
		log.Error(err)
		return
	}
	imageSource, err := ref.NewImageSource(ctx, destinationCtx)
	if err != nil {
		log.Error(err)
		return
	}
	defer func() { _ = imageSource.Close() }()
	rawManifest, _, err := imageSource.GetManifest(ctx, nil)
	if err != nil {
		log.Error(err)
		return
	}
	dvl := DockerV2List{}
	err = json.Unmarshal(rawManifest, &dvl)
	if err != nil {
		log.Error(err)
		return
	}
	if dvl.Manifests != nil && len(dvl.Manifests) > 0 {
		fmt.Println(string(rawManifest))
		return
	}
	// single architecture
	img, err := image.FromUnparsedImage(ctx, destinationCtx, image.UnparsedInstance(imageSource, nil))
	if err != nil {
		log.Error(err)
		return
	}
	outputData, err := img.OCIConfig(ctx)
	out, err := json.MarshalIndent(outputData, "", "    ")
	if err != nil {
		log.Error(err)
		return
	}
	_, err = fmt.Fprintf(os.Stdout, "%s\n", string(out))
	if err != nil {
		log.Error(err)
		return
	}
}

func getImageSha(name string) (string, int, error) {
	ctx := context.Background()
	sha := ""
	size := 0
	imageName := name
	if !strings.HasPrefix(name, "docker://") {
		imageName = "docker://" + name
	}

	destRef, err := alltransports.ParseImageName(imageName)
	if err != nil {
		return sha, size, err
	}

	destinationCtx, err := newSystemContext()
	if err != nil {
		return sha, size, err
	}

	destinationCtx.DockerInsecureSkipTLSVerify = types.NewOptionalBool(true)
	destinationCtx.DockerDaemonInsecureSkipTLSVerify = true

	dig, err := docker.GetDigest(ctx, destinationCtx, destRef)
	if err != nil {
		return sha, size, err
	}
	sha = dig.String()

	imageSource, err := destRef.NewImageSource(ctx, destinationCtx)
	if err != nil {
		return sha, size, err
	}
	mf, _, err := imageSource.GetManifest(ctx, &dig)

	dvs := DockerV2Schema{}
	err = json.Unmarshal(mf, &dvs)
	if err != nil {
		return sha, size, err
	}
	size = len(mf)
	return sha, size, nil
}

func putManifests(manifests string, name string) error {
	imageName := name
	if !strings.HasPrefix(name, "docker://") {
		imageName = "docker://" + name
	}

	destRef, err := alltransports.ParseImageName(imageName)
	if err != nil {
		return err
	}
	destinationCtx, err := newSystemContext()
	if err != nil {
		return err
	}
	destinationCtx.DockerInsecureSkipTLSVerify = types.NewOptionalBool(true)
	destinationCtx.DockerDaemonInsecureSkipTLSVerify = true

	publicDest, err := destRef.NewImageDestination(context.Background(), destinationCtx)
	if err != nil {
		return err
	}
	err = publicDest.PutManifest(context.Background(), []byte(manifests), nil)
	if err != nil {
		return err
	}
	return nil
}

func CreateMultiArchImage(img []ImageArch, target string) error {
	dvl := DockerV2List{
		SchemaVersion: 2,
		MediaType:     DockerV2ListMediaType,
		Manifests:     []Manifest{},
	}
	for _, im := range img {
		sha, size, err := getImageSha(im.Name)
		if err != nil {
			return err
		}
		dvl.Manifests = append(dvl.Manifests, Manifest{
			MediaType: DockerV2Schema2MediaType,
			Digest:    sha,
			Size:      size,
			Platform: struct {
				Architecture string `json:"architecture"`
				OS           string `json:"OS"`
			}{
				Architecture: im.Architecture,
				OS:           im.OS,
			},
		})
	}

	dvlByte, err := json.MarshalIndent(dvl, "", "    ")
	if err != nil {
		return err
	}
	err = putManifests(string(dvlByte), target)
	if err != nil {
		return err
	}
	return nil
}

func (op *Options) Manifests() {
	ias := []ImageArch{}
	for _, img := range op.Args {
		_, after, found := strings.Cut(img, op.Image)
		if !found {
			log.Error(fmt.Sprintf("invalid image name %s", img))
			return
		}
		_, after, found = strings.Cut(after, "-")
		if !found {
			log.Error(fmt.Sprintf("invalid image name %s", img))
			return
		}
		ias = append(ias, ImageArch{
			Name:         img,
			OS:           "linux",
			Architecture: after,
		})
	}

	err := CreateMultiArchImage(ias, op.Image)
	if err != nil {
		log.Error(err.Error())
	}
}

func IsMultiArchManifests(verify bool, imageName string) bool {
	ctx := context.Background()

	destinationCtx, err := newSystemContext()
	if err != nil {
		log.Error(err)
		return false
	}
	if verify {
		destinationCtx.DockerInsecureSkipTLSVerify = types.NewOptionalBool(true)
		destinationCtx.DockerDaemonInsecureSkipTLSVerify = true
	}
	if !strings.HasPrefix(imageName, "docker://") {
		imageName = "docker://" + imageName
	}

	ref, err := alltransports.ParseImageName(imageName)
	if err != nil {
		log.Error(err)
		return false
	}
	imageSource, err := getNewImageSource(ref, ctx, destinationCtx,
		utils.NewRetryOptions(utils.MaxRetryCount, utils.DelayTime))
	if err != nil {
		log.Error(err)
		return false
	}
	rawManifest, _, err := getManifest(imageSource, ctx, utils.NewRetryOptions(utils.MaxRetryCount, utils.DelayTime))
	if err != nil {
		log.Error(err)
		return false
	}
	dvl := DockerV2List{}
	err = json.Unmarshal(rawManifest, &dvl)
	if err != nil {
		log.Error(err)
		return false
	}
	if dvl.Manifests == nil || len(dvl.Manifests) < 1 {
		return false
	}
	return true
}

func getNewImageSource(ref types.ImageReference, ctx context.Context, destinationCtx *types.SystemContext,
	options utils.RetryOptions) (types.ImageSource, error) {
	var imageSource types.ImageSource
	var err error

	for i := 0; i < options.MaxRetry; i++ {
		imageSource, err = ref.NewImageSource(ctx, destinationCtx)
		if err == nil {
			break
		}
		log.Error(fmt.Sprintf("failed to get new image source: %v, retrying...", err))
		time.Sleep(options.Delay)
	}

	return imageSource, err
}

func getManifest(imageSource types.ImageSource, ctx context.Context,
	options utils.RetryOptions) ([]byte, string, error) {
	var rawManifest []byte
	var mediaType string
	var err error

	for i := 0; i < options.MaxRetry; i++ {
		rawManifest, mediaType, err = imageSource.GetManifest(ctx, nil)
		if err == nil {
			break
		}
		log.Error(fmt.Sprintf("failed to get manifest: %v, retrying...", err))
		time.Sleep(options.Delay)
	}

	return rawManifest, mediaType, err
}
